package hpc.exercices.exercice1;

import static org.jocl.CL.CL_CONTEXT_PLATFORM;
import static org.jocl.CL.CL_DEVICE_TYPE_ALL;
import static org.jocl.CL.CL_MEM_COPY_HOST_PTR;
import static org.jocl.CL.CL_MEM_READ_ONLY;
import static org.jocl.CL.CL_TRUE;
import static org.jocl.CL.clBuildProgram;
import static org.jocl.CL.clCreateBuffer;
import static org.jocl.CL.clCreateCommandQueue;
import static org.jocl.CL.clCreateContext;
import static org.jocl.CL.clCreateKernel;
import static org.jocl.CL.clCreateProgramWithSource;
import static org.jocl.CL.clEnqueueNDRangeKernel;
import static org.jocl.CL.clEnqueueReadBuffer;
import static org.jocl.CL.clGetDeviceIDs;
import static org.jocl.CL.clGetPlatformIDs;
import static org.jocl.CL.clReleaseCommandQueue;
import static org.jocl.CL.clReleaseContext;
import static org.jocl.CL.clReleaseKernel;
import static org.jocl.CL.clReleaseMemObject;
import static org.jocl.CL.clReleaseProgram;
import static org.jocl.CL.clSetKernelArg;
import hpc.exercices.Profiler;
import hpc.exercices.Utils;

import java.awt.BorderLayout;
import java.awt.Graphics;
import java.awt.GridLayout;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Random;

import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;

import org.jocl.CL;
import org.jocl.Pointer;
import org.jocl.Sizeof;
import org.jocl.cl_command_queue;
import org.jocl.cl_context;
import org.jocl.cl_context_properties;
import org.jocl.cl_device_id;
import org.jocl.cl_event;
import org.jocl.cl_kernel;
import org.jocl.cl_mem;
import org.jocl.cl_platform_id;
import org.jocl.cl_program;

/**
 * Exercise 1 - Image rotation
 * 
 * @author Hinterleitner Michael, Heily Daniel
 *
 */

public class ImageRotation {

	/*
	 * open cl variables
	 */
	
	private static cl_context context;

	private static cl_command_queue commandQueue;

	private static cl_program program;

	private static cl_kernel kernel;

	private static String path = "images/";
	private static String lena1 = "lena1.png";
	private static String lena2 = "lena2.png";
	private static String goofy = "goofy.png";
	private static String building = "building.png";
	
	private static String outFilename;

	// write image to filesystem true/false - default:false
	private static boolean writeOutputImageInFile = true;

	private static String programSource;

	public static void main(String[] args) {

		// read the kernel code from external file
		try {
			programSource = Utils.readFile("kernelCode/exercise1.cl",
					Charset.defaultCharset());
		} catch (IOException e) {
			System.err.println("kernel source code couldn't be read! Message:"
					+ e.getMessage());
		}
		
		Random r = new Random();
		String  image = goofy;
		
		double angle = r.nextDouble()*360;
		rotateImage(path+image, angle);
		
	}

	private static void rotateImage(String imageName, double angle) {
		
		// read input File and create a Buffered image
		BufferedImage img = null;
		img = readBufferedImage(imageName);

		// get image width and height -> relevant for worksize dimension
		int width = img.getWidth();
		int height = img.getHeight();

		// initialize output image
		BufferedImage outputImage = new BufferedImage(width, height,
				BufferedImage.TYPE_INT_RGB);

		// get the DataBuffer from the input image.
		DataBufferInt dataBufferSrc = (DataBufferInt) img.getRaster()
				.getDataBuffer();

		// create arrays for input and output data
		int dataSrc[] = dataBufferSrc.getData();
		int destArray[] = new int[width * height];

		// create Pointers to input and ouptput data array
		Pointer inputPointer = Pointer.to(dataSrc);
		Pointer outputPointer = Pointer.to(destArray);

		// setup openCL
		final int platformIndex = 0;
		final long deviceType = CL_DEVICE_TYPE_ALL;
		final int deviceIndex = 0;

		// enable exceptions
		CL.setExceptionsEnabled(true);

		// obtain the number of platforms
		int numPlatformsArray[] = new int[1];
		clGetPlatformIDs(0, null, numPlatformsArray);
		int numPlatforms = numPlatformsArray[0];

		// obtain a platform ID
		cl_platform_id platforms[] = new cl_platform_id[numPlatforms];
		clGetPlatformIDs(platforms.length, platforms, null);
		cl_platform_id platform = platforms[platformIndex];

		// initialize the context properties
		cl_context_properties contextProperties = new cl_context_properties();
		contextProperties.addProperty(CL_CONTEXT_PLATFORM, platform);

		// obtain the number of devices for the platform
		int numDevicesArray[] = new int[1];
		clGetDeviceIDs(platform, deviceType, 0, null, numDevicesArray);
		int numDevices = numDevicesArray[0];

		// obtain a device ID
		cl_device_id devices[] = new cl_device_id[numDevices];
		clGetDeviceIDs(platform, deviceType, numDevices, devices, null);
		cl_device_id device = devices[deviceIndex];

		// create a context for the selected device
		context = clCreateContext(contextProperties, 1,
				new cl_device_id[] { device }, null, null, null);

		long properties = 0;
		properties |= CL.CL_QUEUE_PROFILING_ENABLE;
		properties |= CL.CL_QUEUE_OUT_OF_ORDER_EXEC_MODE_ENABLE;

		// create a command-queue for the selected device cl_command_queue
		commandQueue = clCreateCommandQueue(context, device, properties, null);

		// allocate the memory objects for the input- and output data
		cl_mem memObjects[] = new cl_mem[2];
		memObjects[0] = clCreateBuffer(context, CL_MEM_READ_ONLY
				| CL_MEM_COPY_HOST_PTR, Sizeof.cl_int * width * height,
				inputPointer, null);
		memObjects[1] = clCreateBuffer(context, CL_MEM_READ_ONLY
				| CL_MEM_COPY_HOST_PTR, Sizeof.cl_int * width * height,
				outputPointer, null);

		// create the program from the source code
		program = clCreateProgramWithSource(context, 1,
				new String[] { programSource }, null, null);

		// build the program
		clBuildProgram(program, 0, null, null, null, null);

		// create the kernel image_rotate
		kernel = clCreateKernel(program, "image_rotate", null);

		// global worksize (width x length = image size)
		// work with 2 dim global worksize
		long globalWorkSize[] = new long[2];
		globalWorkSize[0] = width;
		globalWorkSize[1] = height;

		// local worksize
		long localWorkSize[] = new long[2];
		localWorkSize[0] = 1;
		localWorkSize[1] = 1;

		// setting arguments for kernel
		clSetKernelArg(kernel, 0, Sizeof.cl_mem, Pointer.to(memObjects[0]));
		clSetKernelArg(kernel, 1, Sizeof.cl_mem, Pointer.to(memObjects[1]));
		clSetKernelArg(kernel, 2, Sizeof.cl_int,
				Pointer.to(new int[] { width }));
		clSetKernelArg(kernel, 3, Sizeof.cl_int,
				Pointer.to(new int[] { height }));
		clSetKernelArg(
				kernel,
				4,
				Sizeof.cl_float,
				Pointer.to(new float[] { (float) Math.sin(Math.toRadians(angle)) }));
		clSetKernelArg(
				kernel,
				5,
				Sizeof.cl_float,
				Pointer.to(new float[] { (float) Math.cos(Math.toRadians(angle)) }));

		cl_event kernel_event = new cl_event();

		// execute the kernel
		clEnqueueNDRangeKernel(commandQueue, kernel, 2, null, globalWorkSize,
				localWorkSize, 0, null, kernel_event);

		// wait for kernel "end" event
		CL.clWaitForEvents(1, new cl_event[] { kernel_event });

		// profiler
		System.out
				.println("Time needed by image_rotate Kernel for rotating image "
						+ imageName + " for " + angle + " degrees");
		Profiler.profile(kernel_event,true);

		// read the output data
		clEnqueueReadBuffer(commandQueue, memObjects[1], CL_TRUE, 0, width
				* height * Sizeof.cl_int, outputPointer, 0, null, null);

		// release kernel, program, and memory objects
		clReleaseMemObject(memObjects[0]);
		clReleaseMemObject(memObjects[1]);
		shutdown();

		// convert output array to Buffered image.
		final int[] a = ((DataBufferInt) outputImage.getRaster()
				.getDataBuffer()).getData();
		System.arraycopy(destArray, 0, a, 0, destArray.length);

		// display input and output image
		displayImages(img, outputImage);

		if (writeOutputImageInFile) {
			// write output image to filesystem
			writeBufferedImage(outputImage,imageName,angle);
		}
	}

	/*
	 * Method for reading a image from the filesystem
	 */
	private static BufferedImage readBufferedImage(String fileName) {
		BufferedImage image = null;
		try {
			image = ImageIO.read(new File(fileName));
		} catch (IOException e) {
			e.printStackTrace();
			return null;
		} 

		int sizeX = image.getWidth();
		int sizeY = image.getHeight();

		BufferedImage result = new BufferedImage(sizeX, sizeY,
				BufferedImage.TYPE_INT_RGB);

		Graphics g = result.createGraphics();
		g.drawImage(image, 0, 0, null);
		g.dispose();
		return result;
	}

	/*
	 * Methode for writing a image to the filesystem
	 */
	private static void writeBufferedImage(BufferedImage outputImage, String imageName, double angle) {
		try {
			outFilename = imageName.replace(path, "").split("\\.")[0] + "_rotated_"
					+ ((int) angle) + ".png";
			System.out.println(outFilename);
			File outputfile = new File(path + outFilename);
			ImageIO.write(outputImage, outFilename.split("\\.")[1], outputfile);
		} catch (IOException e) {
			System.err.println("Image couldn't be saved! Message: "
					+ e.getMessage());
		}
	}

	/*
	 * Method to display the input and output image
	 */
	private static void displayImages(BufferedImage img,
			BufferedImage outputImage) {
		// create layout of mainPanel
		JPanel mainPanel = new JPanel(new GridLayout(1, 0));
		JLabel inputLabel = new JLabel(new ImageIcon(img));
		mainPanel.add(inputLabel, BorderLayout.CENTER);
		JLabel outputLabel = new JLabel(new ImageIcon(outputImage));
		mainPanel.add(outputLabel, BorderLayout.CENTER);

		// create main frame
		JFrame frame = new JFrame("Exercise 1 - ImageRotate");
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.setLayout(new BorderLayout());
		frame.add(mainPanel, BorderLayout.CENTER);
		frame.pack();
		frame.setLocationRelativeTo(null);
		frame.setVisible(true);
	}

	/*
	 * Method to release kernel, program, context and commandqueue
	 */
	private static void shutdown() {
		clReleaseKernel(kernel);
		clReleaseProgram(program);
		clReleaseCommandQueue(commandQueue);
		clReleaseContext(context);
	}

}
